﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Web;

namespace Kiiro.Pop3Client
{
    public delegate void Pop3EventHandler(object source, PopClient.Pop3EventArgs e);

    public class PopClient : IDisposable
    {
        public enum Pop3SessionState
        {
            undefined = 0,
            Disconnected = 1,
            Authorization = 2,
            Transaction = 3,
            Update = 4
        }

        #region Fields
        private TcpClient client;
        private Stream stream;
        private StreamReader reader;
        private string lastResponse;
        private const string CRLF = "\r\n";
        private int readTimeout = 6000;
        private MessageInfoCollection messages = new MessageInfoCollection();
        private List<int> deletedMessageNumbers = new List<int>();
        private List<int> downloadedMessageNumbers = new List<int>();
        #endregion

        #region Properties
        public string ApopTimestamp { get; private set; }
        public string Host { get; set; }
        public int Port { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public bool UseSSL { get; set; }
        public bool AutoReconnect { get; set; }
        public Pop3SessionState State { get; private set; }
        public int ReadTimeout
        {
            get
            {
                return readTimeout;
            }
            set
            {
                readTimeout = value;
                if (stream != null && stream.CanTimeout)
                    stream.ReadTimeout = readTimeout;
            }
        }
        public MailboxInfo Mailbox { get; private set; }
        public MessageInfoCollection Messages
        {
            get { return this.messages; }
        }
        public bool DeleteMessagesAfterDownload { get; set; }
        #endregion

        public PopClient(string host, int port)
        {
            this.Host = host;
            this.Port = port;
            this.ApopTimestamp = "";
            this.UseSSL = false;
            this.AutoReconnect = true;
            this.State = Pop3SessionState.Disconnected;
            this.DeleteMessagesAfterDownload = false;
            this.Mailbox = new MailboxInfo(-1, -1);
            this.messages = new MessageInfoCollection();
        }
        public PopClient(string host, int port, string username, string password)
            : this(host, port)
        {
            this.Username = username;
            this.Password = password;
        }
        ~PopClient()
        {
            if (client != null)
                client.Close();
            if (stream != null)
                stream.Dispose();
            if (reader != null)
                reader.Dispose();
        }

        #region Public Methods

        public string Connect()
        {
            string response = "";
            try
            {
                // connect to the client
                client = new TcpClient(this.Host, this.Port);
                try
                {
                    if (this.UseSSL)
                    {
                        // get ssl stream
                        stream = new SslStream(client.GetStream(), false);

                        // authenticate
                        ((SslStream)stream).AuthenticateAsClient(this.Host);
                    }
                    else
                    {
                        stream = client.GetStream();
                    }
                    stream.ReadTimeout = readTimeout;
                }
                catch (Exception e)
                {
                    if (this.UseSSL)
                        throw new Pop3Exception(String.Concat("Host found but SSL authentication failed. May your mail server does not support SSL.", Environment.NewLine, e.Message), e.InnerException);
                    else
                        throw new Pop3Exception(String.Concat("Connecting to the server failed. May your mail server requires SSL.", Environment.NewLine, e.Message), e.InnerException);
                }
                reader = new StreamReader(stream, Encoding.ASCII);

                // get first response form server
                lastResponse = reader.ReadLine();
                if (lastResponse.StartsWith("+OK"))
                {
                    // try to extract apop timestamp
                    Regex r = new Regex(@"(?<TimeStamp>\x3C[^\x3C\x3E]+\x3E)");
                    Match m = r.Match(lastResponse);
                    if (m.Groups["TimeStamp"].Value != "")
                    {
                        this.ApopTimestamp = m.Groups["TimeStamp"].Value;
                    }
                    else
                    {
                        this.ApopTimestamp = "";
                    }
                    // update session state
                    SetSessionState(Pop3SessionState.Authorization);
                }
                response = lastResponse;
            }
            catch (Pop3Exception e)
            {
                response = String.Concat("An error occured while connecting to ", this.Host, ":", this.Port, (this.UseSSL ? "using SSL " : ""), Environment.NewLine, "Error: ", e.Message);
                throw new Pop3Exception(String.Concat("An error occured while connecting to ", this.Host, ":", this.Port, (this.UseSSL ? "using SSL " : ""), Environment.NewLine, "Error: ", e.Message), e.InnerException);
            }
            return response;
        }

        public void Dispose()
        {
            if (client.Client.Connected)
                client.Client.Disconnect(true);
            client.Close();
            if (stream != null)
                stream.Dispose();
            if (reader != null)
                reader.Dispose();
            SetSessionState(Pop3SessionState.Disconnected);
        }

        public bool Login()
        {
            return Login(this.Username, this.Password);
        }

        public bool Login(bool useApop)
        {
            return Login(this.Username, this.Password, useApop);
        }

        public bool Login(string username, string password)
        {
            return Login(username, password, false);
        }

        public bool Login(string username, string password, bool useApop)
        {
            bool loggedin = false;
            if (useApop && this.ApopTimestamp.IsNotNullOrEmpty())
            {
                // use apop login
                if (APOP(username, password).IsOK())
                {
                    loggedin = true;
                }
            }
            else
            {
                // use user/pass login
                if (USER(username).IsOK())
                {
                    if (PASS(password).IsOK())
                    {
                        loggedin = true;
                    }
                }
            }
            if (loggedin)
            {
                UpdateMailboxStats();
                UpdateMessagesInfo();
            }
            return loggedin;
        }

        public bool Logout()
        {
            if (!DeleteMessagesAfterDownload)
            {
                RSET();
                foreach (int messageNumber in this.deletedMessageNumbers)
                {
                    DELE(messageNumber);
                }
            }
            else
            {
                foreach (int messageNumber in this.downloadedMessageNumbers)
                {
                    DELE(messageNumber);
                }
            }
            return QUIT().IsOK();
        }


        public int GetMessageSize(int messageNumber)
        {
            int messageSize = -1;
            bool success = SendCommand(String.Format("LIST {0}", messageNumber));
            string response = "";
            if (success && (response = GetSingleLineResponse()).IsOK())
            {
                string[] values = response.Split(' ');
                int.TryParse(values[2], out messageSize);
            }
            return messageSize;
        }

        public string GetUid(int messageNumber)
        {
            string uniqueID = "";
            MessageInfo mi = this.messages.FirstOrDefault(m => m.MessageNumber == messageNumber);
            if (mi != null)
            {
                uniqueID = mi.UniqueID;
            }
            else
            {
                bool success = SendCommand(String.Format("UIDL {0}", messageNumber));
                string response = "";
                if (success && (response = GetSingleLineResponse()).IsOK())
                {
                    string[] values = response.Split(' ');
                    uniqueID = values[2];
                }
            }
            return uniqueID;
        }

        public Message GetMessage ( int msgNumber )
        {
            Message msg = new Message ( );

            string message = GetMessageString ( msgNumber );
            Dictionary<string, string> dict = new Message ( ).ParseHeaders ( message );
            MimePart mp = msg.GetMimePart ( message, dict [ "Content-Type" ] );

            msg.MessageString = message;
            msg.From = dict [ "From" ];
            msg.Subject = dict [ "Subject" ];
            msg.Date = Convert.ToDateTime ( dict [ "Date" ] );

            foreach ( MimePart part in mp.Parts )
            {
                if ( part.ContentType.StartsWith ( "text/html" ) )
                    msg.HtmlBody = part.Content;
                if ( part.ContentType.StartsWith ( "text/plain" ) )
                {
                    if ( dict.ContainsKey ( "Content-Transfer-Encoding" ) )
                        msg.TextBody = Utility.FormatPlainText ( part.Content, dict [ "Content-Transfer-Encoding" ] );
                    else
                        msg.TextBody = part.Content;
                }
            }

            if ( msg.TextBody == null )
            {
                if ( dict.ContainsKey ( "Content-Transfer-Encoding" ) )
                {
                    msg.TextBody = Utility.FormatPlainText ( mp.Content, dict [ "Content-Transfer-Encoding" ] );
                    msg.HtmlBody = Utility.FormatPlainText ( mp.Content, dict [ "Content-Transfer-Encoding" ] );
                }
                else
                {
                    msg.TextBody = mp.Content;
                    msg.HtmlBody = mp.Content;
                }
            }

            return msg;
        }

        public string GetMessageString(int messageNumber)
        {
            return RETR(messageNumber);
        }

        public string GetMessageString(string uid)
        {
            int messageNumber = this.messages.GetMessageNumber(uid);
            return GetMessageString(messageNumber);
        }

        public string GetMessageHeaders(int messageNumber)
        {
            return TOP(messageNumber);
        }

        public string GetMessageHeaders(string uid)
        {
            int messageNumber = this.messages.GetMessageNumber(uid);
            return GetMessageHeaders(messageNumber);
        }

        public bool DeleteMessage(int messageNumber)
        {
            if (DELE(messageNumber).IsOK())
            {
                deletedMessageNumbers.Add(messageNumber);
                return true;
            }
            else
                return false;
        }

        public bool DeleteMessage(string uid)
        {
            int messageNumber = this.messages.GetMessageNumber(uid);
            return DeleteMessage(messageNumber);
        }


        #region Standard POP3 Commands
        public string USER(string username)
        {
            bool success = SendCommand(String.Format("USER {0}", username));
            return success ? GetSingleLineResponse() : "";
        }

        public string PASS(string password)
        {
            // send PASS command
            bool success = SendCommand(String.Format("PASS {0}", password));
            string response = success ? GetSingleLineResponse() : "";

            // update session state
            if (response.IsOK())
                SetSessionState(Pop3SessionState.Transaction);
            else if (response.IsERR())
                SetSessionState(Pop3SessionState.Authorization);
            else
                SetSessionState(Pop3SessionState.Disconnected);

            // return server response
            return response;
        }

        public string STAT()
        {
            bool success = SendCommand("STAT");
            return success ? GetSingleLineResponse() : "";
        }

        public string LIST()
        {
            bool success = SendCommand("LIST");
            return success ? GetMultiLineResponse() : "";
        }
        public string LIST(int messageNumber)
        {
            bool success = SendCommand(String.Format("LIST {0}", messageNumber));
            return success ? GetSingleLineResponse() : "";
        }

        public string RETR(int messageNumber)
        {
            bool success = SendCommand(String.Format("RETR {0}", messageNumber));
            return success ? GetMultiLineResponse() : "";
        }

        public string DELE(int messageNumber)
        {
            bool success = SendCommand(String.Format("DELE {0}", messageNumber));
            return success ? GetSingleLineResponse() : "";
        }

        public string RSET()
        {
            bool success = SendCommand("RSET");
            return success ? GetSingleLineResponse() : "";
        }

        public string QUIT()
        {
            // update session state
            SetSessionState(Pop3SessionState.Update);

            // send QUIT command
            bool success = SendCommand("QUIT");
            string response = success ? GetSingleLineResponse() : "";

            // update session state
            if (response.IsOK())
                SetSessionState(Pop3SessionState.Disconnected);
            else
                SetSessionState(Pop3SessionState.undefined);

            // return server response
            return response;
        }

        public string APOP(string username, string password)
        {
            // calculate md5 has
            MD5 md5Hasher = MD5.Create();
            byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(this.ApopTimestamp + password));
            StringBuilder digest = new StringBuilder();
            for (int i = 0; i < data.Length; i++)
            {
                digest.Append(data[i].ToString("x2"));
            }

            // send apop command
            bool success = SendCommand(String.Format("APOP {0} {1}", username, digest.ToString()));
            string response = success ? GetSingleLineResponse() : "";

            // update session state
            if (response.IsOK())
                SetSessionState(Pop3SessionState.Transaction);
            else if (response.IsERR())
                SetSessionState(Pop3SessionState.Authorization);
            else
                SetSessionState(Pop3SessionState.Disconnected);

            // return server response
            return response;
        }

        public string TOP(int messageNumber)
        {
            bool success = SendCommand(String.Format("TOP {0}", messageNumber));
            return success ? GetMultiLineResponse() : "";
        }
        public string TOP(int messageNumber, int numberOfLines)
        {
            bool success = SendCommand(String.Format("TOP {0} {1}", messageNumber, numberOfLines));
            return success ? GetMultiLineResponse() : "";
        }

        public string UIDL()
        {
            bool success = SendCommand("UIDL");
            return success ? GetMultiLineResponse() : "";
        }
        public string UIDL(int messageNumber)
        {
            bool success = SendCommand(String.Format("UIDL {0}", messageNumber));
            return success ? GetSingleLineResponse() : "";
        }

        public string NOOP()
        {
            bool success = SendCommand("NOOP");
            return success ? GetSingleLineResponse() : "";
        }
        #endregion

        #endregion

        #region Events
        public event Pop3EventHandler SessionStateChanging;
        private void OnSessionStateChanging(Pop3EventArgs e)
        {
            if (SessionStateChanging != null)
                SessionStateChanging(this, e);
        }
        public event Pop3EventHandler SessionStateChanged;
        private void OnSessionStateChanged(Pop3EventArgs e)
        {
            if (SessionStateChanged != null)
                SessionStateChanged(this, e);
        }

        public class Pop3EventArgs : EventArgs
        {
            public PopClient.Pop3SessionState SessionState { get; set; }

            public Pop3EventArgs(PopClient.Pop3SessionState currentState)
            {
                this.SessionState = currentState;
            }
        }
        #endregion

        #region Helper Methods

        private bool SendCommand(string command)
        {
            bool success = true;
            try
            {
                byte[] buffer = Encoding.ASCII.GetBytes(command + CRLF);
                stream.Write(buffer, 0, buffer.Length);
            }
            catch (Exception)
            {
                success = false;
            }
            return success;
        }

        private string GetSingleLineResponse()
        {
            string response = "";
            response = reader.ReadLine();
            if (response == null)
                throw new Pop3Exception("Pop3 mail server connection has timed out.");
            return response;
        }

        private string GetMultiLineResponse()
        {
            StringBuilder response = new StringBuilder();
            lastResponse = reader.ReadLine();
            response.AppendLine(lastResponse);
            if (lastResponse.IsOK())
            {
                while (ProcessMultiLineResponse(out lastResponse))
                {
                    response.AppendLine(lastResponse);
                }
                response.AppendLine(lastResponse);
            }
            return response.ToString();
        }

        private bool ProcessMultiLineResponse(out string response)
        {
            response = reader.ReadLine();
            if (response == null)
                throw new Pop3Exception("Pop3 mail server connection has timed out.");
            if (response.StartsWith("."))
            {
                if (response == ".")
                    // multiline responses always end with a single dot in a line
                    return false;
                // if a line starts with a dot it is doubled, the second dot must be removed now
                response = response.Substring(1);
            }
            return true;
        }

        private void SetSessionState(Pop3SessionState state)
        {
            OnSessionStateChanging(new Pop3EventArgs(this.State));
            this.State = state;
            OnSessionStateChanged(new Pop3EventArgs(this.State));
        }

        private bool EnsureSessionState(string command)
        {
            switch (command.Substring(0, 4).ToLower())
            {
                case "user":
                case "pass":
                case "apop":
                    return (this.State == Pop3SessionState.Authorization);
                default:
                    return (this.State == Pop3SessionState.Transaction);
            }
        }

        private bool UpdateMailboxStats()
        {
            string response = "";
            bool success = SendCommand("STAT");
            if (success && (response = GetSingleLineResponse()).IsOK())
            {
                int numberOfMessages = -1;
                int mailboxSize = -1;
                string[] values = response.Split(' ');
                int.TryParse(values[1], out numberOfMessages);
                int.TryParse(values[2], out mailboxSize);
                this.Mailbox = new MailboxInfo(numberOfMessages, mailboxSize);
                return true;
            }
            else
            {
                this.Mailbox = new MailboxInfo(-1, -1);
                return false;
            }
        }

        public bool UpdateMessagesInfo()
        {
            bool messagesUpdated = false;
            MessageInfoCollection messagesInfo = new MessageInfoCollection();
            string response = "";
            // get message numbers and message sizes
            bool success = SendCommand("LIST");
            if (success && GetSingleLineResponse().IsOK())
            {
                while (ProcessMultiLineResponse(out response))
                {
                    int messageNumber = -1;
                    int messageSize = -1;
                    string[] values = response.Split(' ');
                    int.TryParse(values[0], out messageNumber);
                    int.TryParse(values[1], out messageSize);
                    messagesInfo.Add(new MessageInfo(messageNumber, messageSize, ""));
                }
                foreach (MessageInfo mi in messagesInfo)
                {
                    mi.UniqueID = GetUid(mi.MessageNumber);
                }
                this.messages = messagesInfo;
                messagesUpdated = true;
            }
            else
            {
                this.messages = messagesInfo;
            }
            return messagesUpdated;
        }
        #endregion

        #region Exceptions
        public class Pop3Exception : Exception
        {
            public Pop3Exception() { }
            public Pop3Exception(string message) : base(message) { }
            public Pop3Exception(string message, Exception innerException) : base(message, innerException) { }
        }
        #endregion
    }

    public static class ExtensionMethods
    {
        internal static bool IsOK(this string s)
        {
            return s.StartsWith("+OK");
        }
        internal static bool IsERR(this string s)
        {
            return s.StartsWith("-ERR");
        }
        internal static bool IsNotNullOrEmpty(this string s)
        {
            return !String.IsNullOrEmpty(s);
        }
    }

    public class MailboxInfo
    {
        public int NumberOfMessages { get; internal set; }
        public int MailboxSize { get; internal set; }

        public MailboxInfo(int numberOfMessages, int mailboxSize)
        {
            this.NumberOfMessages = numberOfMessages;
            this.MailboxSize = mailboxSize;
        }
    }
    public class MessageInfo
    {
        public int MessageNumber { get; internal set; }
        public int Size { get; internal set; }
        public string UniqueID { get; internal set; }

        public MessageInfo() { }
        public MessageInfo(int messageNumber, int size, string uniqueID)
        {
            this.MessageNumber = messageNumber;
            this.Size = size;
            this.UniqueID = uniqueID;
        }
    }
    public class MessageInfoCollection : List<MessageInfo>
    {
        public string GetUniqueID(int messageNumber)
        {
            MessageInfo mi = this.First(m => m.MessageNumber == messageNumber);
            if (mi != null)
                return mi.UniqueID;
            else
                return "";
        }
        public int GetMessageNumber(string uid)
        {
            MessageInfo mi = this.First(m => m.UniqueID == uid);
            if (mi != null)
                return mi.MessageNumber;
            else
                return -1;
        }
    }
}